package com.google.code.wicket.auth.component.value.resolver.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;

/**
 * An {@link AnnotationValueRetrievalStrategy} that takes multi-value annotation
 * parameters and returns them as collections of values.
 * <p>
 * <code><pre>
 * &#64;Retention(RetentionPolicy.RUNTIME)
 * &#64;Target(ElementType.TYPE)
 * public &#64;interface MyAnnotation {
 *     String[] value() default {};
 * }
 * 
 * &#64;MyAnnotation("one","two","three")
 * public class MyClass{
 *  // ...
 * }
 * 
 * {@link AnnotationCache} cache = AnnotationCache.forAnnotationType(MyAnnotation.class);
 * Collection<String> values =
 *     MultiAnnotationValueRetrievalStrategy
 *         .forAnnotation(MyAnnotation.class)
 *         .retrieveAnnotationValue(cache.getAnnotationForType(MyClass.class));
 * // values will be a collection of ("one", "two", "three") 
 * </pre></code>
 * 
 * @param <A>
 *            the annotation type
 * @param <T>
 *            the return type
 */
public class MultiAnnotationValueRetrievalStrategy<A extends Annotation, T>
        implements AnnotationValueRetrievalStrategy<A, Collection<T>> {

    /**
     * Factory method to get a resolver based on the value() method / property
     * of the specified annotation.
     * 
     * @param <A>
     *            annotation type
     * @param <T>
     *            return type
     * @param annotationClass
     *            the annotation class
     * @param returnType
     *            the return type class
     * @return a resolver based on the annotation
     */
    public static <A extends Annotation, T> MultiAnnotationValueRetrievalStrategy<A, T> forAnnotation(
            final Class<A> annotationClass, final Class<T> returnType) {
        return new MultiAnnotationValueRetrievalStrategy<A, T>(annotationClass,
                returnType);
    }

    /**
     * Factory method to get a resolver based on the specified method / property
     * of the specified annotation.
     * 
     * @param <A>
     *            annotation type
     * @param <T>
     *            return type
     * @param annotationClass
     *            the annotation class
     * @param returnType
     *            the return type class
     * @param methodName
     *            the annotation method / property name to use
     * @return a resolver based on the annotation
     */
    public static <A extends Annotation, T> MultiAnnotationValueRetrievalStrategy<A, T> forAnnotation(
            final Class<A> annotationClass, final Class<T> returnType,
            final String methodName) {
        return new MultiAnnotationValueRetrievalStrategy<A, T>(annotationClass,
                returnType, methodName);
    }

    private final Class<T> returnType;

    private Method valueMethod;
    private boolean isEnum;

    /**
     * Protected constructor (use factory method
     * {@link #forAnnotation(Class, Class)} or write a subclass).
     * 
     * @param annotationClass
     * @param returnType
     */
    protected MultiAnnotationValueRetrievalStrategy(
            final Class<A> annotationClass, final Class<T> returnType) {
        this(annotationClass, returnType, "value");
    }

    /**
     * Protected constructor (use factory method
     * {@link #forAnnotation(Class, Class, String)} or write a subclass).
     * 
     * @param annotationClass
     * @param returnType
     * @param methodName
     */
    protected MultiAnnotationValueRetrievalStrategy(
            final Class<A> annotationClass, final Class<T> returnType,
            final String methodName) {
        this.returnType = returnType;
        this.isEnum = returnType.isEnum();
        try {
            this.valueMethod = annotationClass.getMethod(methodName);
            final Class<?> returnArrayType = this.valueMethod.getReturnType();
            if (!returnArrayType.isArray()) {
                throw new IllegalArgumentException("Method " + methodName
                        + " must return an array of " + returnType);
            } else if (!returnType.isAssignableFrom(returnArrayType
                    .getComponentType())) {
                throw new IllegalArgumentException(
                        MessageFormat
                                .format(
                                        "Return type {0} is not compatible with requested return type (array of {1})",
                                        this.valueMethod.getReturnType(),
                                        returnType));
            }
        } catch (final SecurityException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Can''t access the ''{1}'' method of {0}", annotationClass,
                    methodName), e);
        } catch (final NoSuchMethodException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Class {0} doesn''t have a ''{2}'' method: {1}",
                    annotationClass, annotationClass, methodName), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public Collection<T> retrieveAnnotationValue(final A annotation) {
        try {

            return (annotation == null ? Collections.<T> emptySet() : this
                    .coll((T[]) this.valueMethod.invoke(annotation)));
        } catch (final IllegalAccessException e) {
            throw new IllegalStateException(MessageFormat.format(
                    "Can''t access property method {0} in annotation {1}",
                    this.valueMethod, annotation), e);
        } catch (final InvocationTargetException e) {
            throw new IllegalStateException(MessageFormat.format(
                    "Can''t access property method {0} in annotation {1}",
                    this.valueMethod, annotation), e);
        }
    }

    @SuppressWarnings("unchecked")
    private Collection<T> coll(final T[] data) {
        Collection<T> result;
        if (this.isEnum) {
            result = EnumSet.noneOf((Class<? extends Enum>) this.returnType);
        } else {
            result = new HashSet<T>();
        }
        if (data != null) {
            for (final T item : data) {
                result.add(item);
            }
        }
        return result;
    }
}
